package cs.android.http;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.cookie.Cookie;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HTTP;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;

import static cs.android.lang.AndroidLang.asString;
import static cs.java.lang.Lang.between;
import static cs.java.lang.Lang.exception;
import static cs.java.lang.Lang.info;
import static cs.java.lang.Lang.is;
import static cs.java.lang.Lang.iterate;
import static cs.java.lang.Lang.json;
import static cs.java.lang.Lang.list;
import static cs.java.lang.Lang.no;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import cs.java.collections.List;
import cs.java.collections.Map;
import cs.java.collections.Mapped;
import cs.java.json.JSONArray;
import cs.java.json.JSONContainer;
import cs.java.json.JSONObject;
import cs.java.json.JSONType;
import cs.java.lang.Base;
import cs.java.model.Credentials;
import cs.java.net.Url;

public class HttpClientImpl extends Base implements HttpClient {

	private boolean showInfoOfResponseString;
	private DefaultHttpClient client;
	private HttpResponse response;
	private HttpUriRequest request;
	private BasicCookieStore cookieStore;
	private Url url = new Url();
	private CredentialsProvider credentials;
	private int[] exceptionStatusCodes;

	public HttpClientImpl() {}

	public HttpClientImpl(String url) {
		this.url.setBaseUrl(url);
	}

	private HttpEntity createEntity(JSONType data) {
		info("Sending Entity:", data.toJSON());
		try {
			StringEntity entity = new StringEntity(data.toJSON(), HTTP.UTF_8);
			entity.setContentType("application/json");
			return entity;
		} catch (UnsupportedEncodingException e) {
			throw exception(e);
		}
	}

	private HttpEntity createEntity(Map<String, String> map) {
		try {
			List<NameValuePair> nameValuePairs = list();
			for (Mapped<String, String> item : iterate(map))
				nameValuePairs.add(new BasicNameValuePair(item.key(), item.value()));
			UrlEncodedFormEntity entity = new UrlEncodedFormEntity(nameValuePairs);
			info("Sending Entity:", asString(entity.getContent()));
			return entity;
		} catch (IOException e) {
			throw exception(e);
		}
	}

	private HttpResponse execute(HttpUriRequest request) {
		this.request = request;
		try {
			client = new DefaultHttpClient();
			if (is(cookieStore)) client.setCookieStore(cookieStore);
			HttpParams params = new BasicHttpParams();
			HttpClientParams.setRedirecting(params, true);
			client.setParams(params);

			if (is(credentials)) client.setCredentialsProvider(credentials);
			info("Sending", request.getURI(), request.getMethod());
			response = client.execute(request);
			info("Response Status Line", response.getStatusLine());
		} catch (Exception e) {
			throw exception(e);
		}
		for (int statusCode : iterate(exceptionStatusCodes))
			if (getResponseStatus() == statusCode) throw new HttpStatusException(statusCode);
		return response;
	}

	private HttpClient executePost(HttpEntity entity) {
		HttpPost post = new HttpPost(url.toString());
		post.setEntity(entity);
		execute(post);
		return this;
	}

	private java.util.List<Cookie> getHttpCookies() {
		return client.getCookieStore().getCookies();
	}

	private JSONContainer getResponseJson() {
		return json(getResponseString());
	}

	public HttpClient add(String key, Object value) {
		url.add(key, value);
		return this;
	}

	public HttpClient addCookie(Cookie cookie) {
		if (no(cookieStore)) cookieStore = new BasicCookieStore();
		cookieStore.addCookie(cookie);
		return this;
	}

	public HttpClient executeDelete() {
		HttpDelete post = new HttpDelete(url.toString());
		execute(post);
		return this;
	}

	public HttpClient executeGet() {
		execute(new HttpGet(url.toString()));
		return this;
	}

	public HttpClient executePatch(JSONObject data) {
		HttpPost post = new HttpPost(url.toString()) {
			@Override public String getMethod() {
				return "PATCH";
			}
		};
		post.setEntity(createEntity(data));
		execute(post);
		return this;
	}

	public HttpClient executePost(JSONType data) {
		executePost(createEntity(data));
		return this;
	}

	public HttpClient executePost(Map<String, String> arguments) {
		executePost(createEntity(arguments));
		return this;
	}

	public Cookie getCookie(int index) {
		if (getHttpCookies().size() > index) return getHttpCookies().get(index);
		return null;
	}

	public Cookie getCookie(String name) {
		for (Cookie cookie : getCookies())
			if (cookie.getName().equals(name)) return cookie;
		return null;
	}

	public List<Cookie> getCookies() {
		return list(getHttpCookies());
	}

	public HttpResponse getResponse() {
		return response;
	}

	public JSONArray getResponseArray() {
		JSONContainer json = getResponseJson();
		if (is(json)) return json.asArray();
		return null;
	}

	public Bitmap getResponseBitmap() {
		try {
			if (!isResponseOk()) return null;
			final HttpEntity entity = getResponse().getEntity();
			if (is(entity)) {
				InputStream stream = null;
				try {
					stream = new FlushedInputStream(entity.getContent());
					return BitmapFactory.decodeStream(stream);
				} finally {
					if (is(stream)) stream.close();
					entity.consumeContent();
				}
			}
		} catch (Exception e) {
			request.abort();
		}
		return null;
	}

	public JSONObject getResponseObject() {
		JSONContainer response = getResponseJson();
		if (is(response)) return response.asObject();
		return null;
	}

	public int getResponseStatus() {
		return response.getStatusLine().getStatusCode();
	}

	public String getResponseString() {
		try {
			String content = asString(response.getEntity().getContent());
			if (showInfoOfResponseString) info("Retrived Content", content);
			return content;
		} catch (Exception e) {
			throw exception(e);
		}
	}

	public boolean isResponseOk() {
		return between(getResponseStatus(), 200, 300);
	}

	public HttpClient setCredentials(Credentials value) {
		return setCredentials(value.getUsername(), value.getPassword());
	}

	public HttpClient setCredentials(CredentialsProvider provider) {
		credentials = provider;
		return null;
	}

	public HttpClient setCredentials(String username, String password) {
		credentials = new BasicCredentialsProvider();
		credentials.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));
		return this;
	}

	public void setShowResponseInfo(boolean showInfoOfResponseString) {
		this.showInfoOfResponseString = showInfoOfResponseString;
	}

	public HttpClient setUrl(String url) {
		this.url.setBaseUrl(url);
		return this;
	}

	public HttpClient setUrl(Url url) {
		this.url = url;
		return this;
	}

	@Override public void throwExceptionOn(int... statusCodes) {
		exceptionStatusCodes = statusCodes;
	}

	private class FlushedInputStream extends FilterInputStream {
		public FlushedInputStream(InputStream inputStream) {
			super(inputStream);
		}

		@Override public long skip(long n) throws IOException {
			long totalBytesSkipped = 0L;
			while (totalBytesSkipped < n) {
				long bytesSkipped = in.skip(n - totalBytesSkipped);
				if (bytesSkipped == 0L) {
					int oneByte = read();
					if (oneByte < 0) break; // we reached EOF
					bytesSkipped = 1; // we read one byte
				}
				totalBytesSkipped += bytesSkipped;
			}
			return totalBytesSkipped;
		}
	}

}
